﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;

namespace Black.NBT
{
    /// <summary>
    /// Représente un TAG_Compound
    /// </summary>
    public class TagCompound : Tag, ITagHierarchicalData, ICloneable
    {
        #region Propriétés

        /// <summary>
        /// Obtient le nombre d'élément
        /// </summary>
        public int Count
        {
            get
            {
                return (this.Items.Count);
            }
        }

        /// <summary>
        /// Obtient la liste des éléments
        /// </summary>
        public ObservableCollection<Tag> Items
        {
            get
            {
                return (this._items);
            }
            private set
            {
                if (this._items != value)
                {
                    this._items = value;
                    this.OnPropertyChanged();
                }
            }
        }

        /// <summary>
        /// Obtient ou définit un élément de la liste
        /// </summary>
        /// <param name="index">Index de l'élément</param>
        /// <returns>Instance de l'index</returns>
        public Tag this[int index]
        {
            get
            {
                if (index < 0 || index >= this.Count)
                    throw (new IndexOutOfRangeException());

                return (this.Items[index]);
            }
            set
            {
                if (index < 0 || index >= this.Count)
                    throw (new IndexOutOfRangeException());

                this.Items[index] = value;
            }
        }

        /// <summary>
        /// Obtient ou définit un élément de la liste
        /// </summary>
        /// <param name="name">Nom de l'élément</param>
        /// <returns>Instance du nom</returns>
        public Tag this[string name]
        {
            get
            {
                var result = (from p in this.Items
                              where p.Name == name
                              select p).FirstOrDefault();

                return (result);
            }
            set
            {
                for (int i = 0; i < this.Count; i++)
                {
                    if (this.Items[i].Name == name)
                    {
                        this.Items[i] = value;
                        return;
                    }
                }

                throw (new ArgumentException(string.Format("The name '{0}' does not exist in the collection.", name), "name"));
            }
        }

        #endregion

        #region Variables d'instances

        private ObservableCollection<Tag> _items;

        #endregion

        #region Constructeurs

        /// <summary>
        /// Constructeur de la classe
        /// </summary>
        /// <param name="name">Nom du tag</param>
        public TagCompound(string name)
            : base(name)
        {
            this.TagType = NBT.TagType.Compound;
            this._items = new ObservableCollection<Tag>();
        }

        #endregion

        #region Fonctions publiques

        /// <summary>
        /// Vérifie si il existe un élément nommé
        /// </summary>
        /// <param name="name">Nom a tester</param>
        /// <returns>TRUE l'élément existe, FALSE sinon</returns>
        public bool Contains(string name)
        {
            var result = (from p in this._items
                          where p.Name == name
                          select p).FirstOrDefault();

            return (result != null);
        }

        /// <summary>
        /// Vérifie si il existe un élément nommé d'un certain type
        /// </summary>
        /// <param name="name">Nom a tester</param>
        /// <typeparam name="T">Type a tester</typeparam>
        /// <returns>TRUE l'élément existe, FALSE sinon</returns>
        public bool Contains<T>(string name) where T : Tag
        {
            var result = (from p in this._items
                          where p.Name == name
                          && p.GetType() == typeof(T)
                          select p).FirstOrDefault();

            return (result != null);
        }

        /// <summary>
        /// Vérifie si il existe un élément nommé d'un certain type
        /// </summary>
        /// <param name="name">Nom a tester</param>
        /// <typeparam name="type">Type a tester</typeparam>
        /// <returns>TRUE l'élément existe, FALSE sinon</returns>
        public bool Contains(string name, TagType type)
        {
            var result = (from p in this._items
                          where p.Name == name
                          && p.TagType == type
                          select p).FirstOrDefault();

            return (result != null);
        }

        #endregion

        #region Fonctions internes

        /// <summary>
        /// Ecrit l'instance sur le stream
        /// </summary>
        /// <param name="stream">Instance du flux</param>
        /// <param name="fromList">Si l'écriture vient d'une liste</param>
        internal override void WriteToStream(MemoryStream stream, bool fromList)
        {
            // Si pas d'une liste on inscrit tout 

            if (!fromList)
            {
                // Ecriture du type

                stream.WriteByte((byte)this.TagType);

                // Ecriture du nom

                NBTFile.WriteString(stream, this.Name);
            }

            // Ecriture des éléménts

            foreach (Tag item in this.Items)
                item.WriteToStream(stream);

            // Ecriture de la balise de fin

            TagEnd.WriteTagEnd(stream);
        }

        #endregion

        #region Fonctions statiques

        /// <summary>
        /// Lit le tag dans le flux
        /// </summary>
        /// <param name="stream">Instance du flux placé</param>
        /// <returns>Instance du Tag</returns>
        public static TagCompound Read(MemoryStream stream)
        {
            return (TagCompound.Read(stream, true));
        }

        /// <summary>
        /// Lit le tag dans le flux
        /// </summary>
        /// <param name="stream">Instance du flux placé</param>
        /// <param name="withName">TRUE récupère le nom, FALSE sinon</param>
        /// <returns>Instance du Tag</returns>
        public static TagCompound Read(MemoryStream stream, bool withName)
        {
            List<Tag> list = new List<Tag>();

            // On récupère son nom

            string name = string.Empty;

            if (withName)
                name = Tag.ReadName(stream);

            // Création de l'instance

            TagCompound item = new TagCompound(name);

            // Récupération de son contenu

            bool isEnd = false;

            while (!isEnd)
            {
                // Lecture du prochain tag

                int indexTag = (int)stream.ReadByte();

                // Extraction du tag

                switch ((TagType)indexTag)
                {
                    case (TagType.Byte):
                        list.Add(TagByte.Read(stream));
                        break;

                    case (TagType.ByteArray):
                        list.Add(TagByteArray.Read(stream));
                        break;

                    case (TagType.Compound):
                        list.Add(TagCompound.Read(stream));
                        break;

                    case (TagType.Double):
                        list.Add(TagDouble.Read(stream));
                        break;

                    case (TagType.End):
                        isEnd = true;
                        break;

                    case (TagType.Float):
                        list.Add(TagFloat.Read(stream));
                        break;

                    case (TagType.Int):
                        list.Add(TagInt.Read(stream));
                        break;

                    case (TagType.List):
                        list.Add(TagList.Read(stream));
                        break;

                    case (TagType.Long):
                        list.Add(TagLong.Read(stream));
                        break;

                    case (TagType.Short):
                        list.Add(TagShort.Read(stream));
                        break;

                    case (TagType.String):
                        list.Add(TagString.Read(stream));
                        break;

                    default:
                        throw (new InvalidDataException("Read invalid or unknow Tag Data Type."));
                }
            }

            // On tri les enfants

            list.Sort();

            // Affectation de la liste

            foreach (Tag tag in list)
                item.Items.Add(tag);

            // On retourne la valeur

            return (item);
        }

        #endregion

        #region Clonage

        /// <summary>
        /// Clone l'instance
        /// </summary>
        /// <returns>Nouvelle instance</returns>
        object ICloneable.Clone()
        {
            return (this.Clone());
        }

        /// <summary>
        /// Clone l'instance
        /// </summary>
        /// <returns>Nouvelle instance</returns>
        public virtual TagCompound Clone()
        {
            TagCompound clone = this.MemberwiseClone() as TagCompound;

            clone.Items = new ObservableCollection<Tag>();

            foreach (ICloneable item in this.Items)
                clone.Items.Add((Tag)item.Clone());

            return (clone);
        }

        #endregion
    }
}
